-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Find the program using PATHEXT #37381
Find the program using PATHEXT #37381
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @aturon (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
Windows relies on path extensions to resolve commands, extensions are found in the `PATHEXT` environment variable. We are adding a new function called `Command:find_program()` that will return the `Path` of the program found using the above env variable. Closes rust-lang#37380 Signed-off-by: Salim Afiune <afiune@chef.io>
3608d00
to
40ed572
Compare
PATHEXT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fascinating! Learn something new every day...
if let Some(exts) = self.env.get("PATHEXT") { | ||
for ext in split_paths(&exts) { | ||
let ext_str = pathext.to_str().unwrap().trim_matches('.'); | ||
let path = path.join(self.program.to_str().unwrap()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I think you just need to call with_extension
as otherwise this is joining self.program
onto the root path twice I think.
} else { | ||
// Windows relies on path extensions to resolve commands. | ||
// Path extensions are found in the PATHEXT environment variable. | ||
if let Some(exts) = self.env.get("PATHEXT") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this lookup could be hoisted out?
@alexcrichton Thank you so much for your feedback! I will have plenty of time (on an airplane) tomorrow morning so I'll look at this and do some modifications. Also I will add some tests! 😄 |
This new simple fun will lookup for a specific variable within `self.env` Signed-off-by: Salim Afiune <afiune@chef.io>
Signed-off-by: Salim Afiune <afiune@chef.io>
@alexcrichton I have added the |
} else { | ||
// Windows relies on path extensions to resolve commands. | ||
// Path extensions are found in the PATHEXT environment variable. | ||
if let Some(exts) = env_lookup(self.env.as_ref(), "PATHEXT") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh here self.env.as_ref()
is already bound above as env
(e.g. we know it's Some
already). You can change this to just env.get
, but I was thinking that we'd just want to hoist this out of the loop here so it's only executed once instead of once-per-path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im with you, didn't get it the first time. Changed already! Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I will delete the env_lookup()
fn though..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also yeah for adding a test the best place to add that would likely be src/test/run-pass
and then add a new folder in there with a .rs
file that's the test and a .foo
file fixture which you can read as well.
// Path extensions are found in the PATHEXT environment variable. | ||
if let Some(exts) = env_lookup(self.env.as_ref(), "PATHEXT") { | ||
for ext in split_paths(&exts) { | ||
let ext_str = pathext.to_str().unwrap().trim_matches('.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid an unwrap()
here and just skip anything that can't be turned into a string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me! What about using to_string_lossy()
instead? Something like this:
for ext in split_paths(&exts) {
let ext_str = ext.to_string_lossy();
let path = path.with_extension(
ext_str.trim_matches('.')
);
if fs::metadata(&path).is_ok() {
return Some(path.into_os_string())
}
}
Instead of the `env_lookup()` fn we should just use the `HashMap::get()` Signed-off-by: Salim Afiune <afiune@chef.io>
Im working on the tests @alexcrichton .. Thank you for all your feedback. Let me know if you have more comments on the code. 👍 |
Looks good to me! Feel free to just ping me when a test is added and I'll r+ |
let ext_str = ext.to_string_lossy(); | ||
let path = path.with_extension( | ||
ext_str.trim_matches('.') | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this could fit on oneline?
// Path extensions are found in the PATHEXT environment variable. | ||
if let Some(exts) = env_pathext { | ||
for ext in split_paths(&exts) { | ||
let ext_str = ext.to_string_lossy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we'll want to call to_str
here to not misinterpret OS strings (e.g. lossy may change the meaning), but you can just skip anything that's None
. You can probably do:
for ext in split_paths(&ext).filter_map(|e| e.to_str()) {
// ...
}
@@ -141,12 +138,34 @@ impl Command { | |||
.with_extension(env::consts::EXE_EXTENSION); | |||
if fs::metadata(&path).is_ok() { | |||
return Some(path.into_os_string()) | |||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We typically try to avoid rightward drift where possible, so because of the return
above you can omit this else
, de-indenting what's below. You can also change what's below to:
let exts = match env_pathext {
Some(e) => e,
None => continue,
};
// ...
to get rid of another layer of indentation.
I don't think this is a good idea. |
Signed-off-by: Salim Afiune <afiune@chef.io>
@ollie27 Oh Yeah!! 👍 Batch files MUST use |
Signed-off-by: Salim Afiune <afiune@chef.io>
fn command_on_path_found() { | ||
let c = command_with_pathext("bin"); | ||
let bat = canonicalize("./src/test/run-pass/process_command/fixtures/bin/bin.bat"); | ||
assert_eq!(bat.ok(), c.find_program()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm unfortunately the find_program
function here is a private implementation detail of Command
so this function I don't think will pass on Windows :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexcrichton I made it to be public, just as Command.cmd()
and Command.env()
.. I wonder if it should be private as you mentioned and instead test within the Module? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah the public function you added is actually an internal implementation detail that's wrapped with a std::process::Command
, so it won't be accessible here.
But yeah moving the test to that module would be ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! Im working on it
If the program is a BATCH file CreateProcess requires to start the command interpreter by setting ApplicationName to `cmd.exe` and CommandLine to the following arguments: `/c` plus the name of the batch file. Signed-off-by: Salim Afiune <afiune@chef.io>
@ollie27 interesting! @afiune the only reason we have this probing logic right now is to read the child's |
We need to move this tests to live inside the module since the `find_program` fn is a private implementation. Signed-off-by: Salim Afiune <afiune@chef.io>
Simple fn that will convert a `OsString` into a `Vec<u16>`. Very useful when you need to interact with WindowsAPI. Signed-off-by: Salim Afiune <afiune@chef.io>
@alexcrichton I don't think is that simple... In Windows it is very important the extension and that is why All this information is here: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx As it states:
So you can run other things that are not EXE by supplying an interpreter ( |
Updating the tests to match the modifications made. The `make_command_line` fn now returns two variables, the application_name and the command_line. It will detect when the program is a batch script or not and use the `cmd.exe` as the interpreter. Signed-off-by: Salim Afiune <afiune@chef.io>
3ca9a91
to
e9e960e
Compare
No, As @alexcrichton mentioned, the code for finding the binary that's modified in this PR is part of a hack to read the child's
|
@alexcrichton @ollie27 that makes sense. I didn't know about that child proc information/details. I have a couple of ideas:
I get the point that ollie27 says about |
I'm not sure it's worth the added complexity to do anything special for batch files as it's not unreasonable to have to use |
🤔 I might disagree with that; It is like if we would want to execute Unix scripts like In a Windows terminal if you have a |
I personally believe that The reason that running a |
Ah so yes if @afiune does passing |
@alexcrichton If there are strong feeling about not adding some extra functionality I think it would be ok for me to write a Command.new(find_program("git")?).spawn() Where I think my point was to make it easier for users to be able to use the In other news, I discovered that you can actually do: Command.new("C:/program/bin.bat").spawn() And it works! So |
Basically the intention here is to mirror the semantics of If that works then it would disagree with this comment indicating that |
ping @afiune, any updates here? |
@alexcrichton Thank you for following up!! 😄 The scenario you put above does not work. Some facts are:
That means that this examples will work with the current implementation: // Passing the file name with extension will search for it in the PATH
Command.new("bin.bat")
// Passing the full path will not search, obviously :)
Command.new("C:\program\bin.bat") What doesn't work because // Even though the `bin.bat` is inside one of the dirs defined in PATH, we wont find it.
Command.new("bin") Long story short, we can execute |
Ok, thanks for the investigation! It sounds like though if |
Ping: it's been a month, any chance we can move this forward? |
It sounds like this behavior is specific to |
Yup, it does. |
Windows relies on path extensions to resolve commands, extensions
are found in the
PATHEXT
environment variable. We are adding a newfunction called
Command:find_program()
that will return thePath
ofthe program found using the above env variable.
Closes #37380
Signed-off-by: Salim Afiune afiune@chef.io